using System;
using System.Collections.Generic;
using System.Text;
using Zsa.Emulation.Memory;
using Zsa.Emulation.Processor;
using System.IO;
using Zsa.Emulation.PPU;
using Zsa.Emulation.Cartridge;

namespace Zsa.Emulation.Mappers
{

    public delegate void BankChangeHanlder ();

    /// <summary>
    /// Base class for all mappers.
    /// </summary>
    public abstract partial class Mapper
    {
        #region Fields
        /// <summary>
        /// The mapper information.
        /// </summary>
        private readonly MapperInformation _MapperInformation;

        /// <summary>
        /// The game cart.
        /// </summary>
        private Cart _Cart;

        /// <summary>
        /// The cartridge file info.
        /// </summary>
        private CartridgeFileInfo _CartfileInfo;

        /// <summary>
        /// The video rom offsets.
        /// </summary>
        private int[] _VideoRomOffsets;

        /// <summary>
        /// The program rom offsets.
        /// </summary>
        private int[] _ProgramRomOffsets;

        #region Memory Banks
        /// <summary>
        /// The first program rom.
        /// </summary>
        protected MapperMemoryBank _ProgramRomOne;

        /// <summary>
        /// The second program rom.
        /// </summary>
        protected MapperMemoryBank _ProgramRomTwo;

        /// <summary>
        /// The first char rom.
        /// </summary>
        protected PatternTable _VideoRomOne;

        /// <summary>
        /// The second char rom.
        /// </summary>
        protected PatternTable _VideoRomTwo;

        #endregion Memory Banks
        
        #endregion Fields

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="Mapper"/> class.
        /// </summary>
        /// <param name="mapperName">Name of the mapper.</param>
        /// <param name="mapperDescription">The mapper description.</param>
        /// <param name="mapperTypeNumber">The mapper type number.</param>
        protected Mapper(string mapperName, string mapperDescription,
            MapperTypeNumber mapperTypeNumber)
        {
            _MapperInformation = new MapperInformation(
                mapperName, mapperDescription, this.GetType(),
                mapperTypeNumber);
        }
        #endregion Constructor

        #region Properties

        #region Cartridge 
        /// <summary>
        /// The game cart.
        /// </summary>
        public Cart GameCart
        {
            get
            {
                return _Cart;
            }
        }

        /// <summary>
        /// Gets the cartridge file info.
        /// </summary>
        /// <value>The cartridge file info.</value>
        public CartridgeFileInfo CartInfo
        {
            get
            {
                return _CartfileInfo;
            }
        }
        #endregion Cartridge

        #region Memory Banks
        /// <summary>
        /// Gets the PRG rom bank one.
        /// </summary>
        /// <value>The PRG rom bank one.</value>
        public MemoryBankStream PrgRomBankOne
        {
            get
            {
                return _ProgramRomOne;
            }
        }

        /// <summary>
        /// Gets the PRG rom bank two.
        /// </summary>
        /// <value>The PRG rom bank two.</value>
        public MemoryBankStream PrgRomBankTwo
        {
            get
            {
                return _ProgramRomTwo;
            }
        }

        /// <summary>
        /// Gets the CHR rom.
        /// </summary>
        /// <value>The CHR rom.</value>
        public MemoryBankStream ChrRomOne
        {
            get
            {
                return _VideoRomOne;
            }
        }

        /// <summary>
        /// Gets the CHR rom two.
        /// </summary>
        /// <value>The CHR rom two.</value>
        public MemoryBankStream ChrRomTwo
        {
            get
            {
                return _VideoRomTwo;
            }
        }
        #endregion Memory Banks

        #region Mappper Info Properties
        /// <summary>
        /// The name of the mapper.
        /// </summary>
        /// <value>The mapper info.</value>
        public virtual MapperInformation MapperInfo
        {
            get
            {
                return _MapperInformation;

            }
        }

        /// <summary>
        /// The name of the mapper.
        /// </summary>
        /// <value></value>
        public string MapperName
        {
            get
            {
                return _MapperInformation.MapperName;
            }
        }

        /// <summary>
        /// Gets the mapper description.
        /// </summary>
        /// <value>The mapper description.</value>
        public string MapperDescription
        {
            get
            {
                return _MapperInformation.MapperDescription;
            }
        }

        /// <summary>
        /// The mapper type enum.
        /// </summary>
        /// <value></value>
        public MapperTypeNumber MapperTypeNumber
        {
            get
            {
                return _MapperInformation.MapperTypeNumber;
            }
        }

        #endregion Mappper Info Properties

        #endregion Properties
        
        #region Methods
        /// <summary>
        /// Initializes the mmc and resets the banks.
        /// </summary>
        public void Init(Cart cart)
        {
            // Check if the cart if null.
            if (cart == null)
            {
                throw new NullReferenceException("The cartridge cannot be null !");
            }
            // init the cart.
            OnInit(cart);
            // Make sure everything is here.
            if (_ProgramRomOne == null || _ProgramRomTwo == null || 
                _VideoRomOne == null || _VideoRomTwo == null )
            {
                throw new MapperInitializationException();
            }
            _Cart = cart;
            _CartfileInfo = cart.FileInfo;
            // Write the pattern table.
#if DEBUG
                    WritePatternImages();
#endif

        }

        /// <summary>
        /// Inits the specified cart.
        /// </summary>
        /// <param name="cart">The cart.</param>
        public virtual void OnInit(Cart cart)
        {
            // Get the cartridge file info.
            CartridgeFileInfo _FileInfo = cart.FileInfo;

            // Open the cart.
            if (!cart.IsOpen)
            {
                cart.OpenCart();
            }
            // The cartridge stream.
            Stream _CartStream = cart.CartridgeStream;
            // Create a memory stream for the program rom.
            MemoryStream _ProgramStream = CreateMemoryStream(RomBankType.ProgramRom, _CartStream, _FileInfo);
            // Create a memory stream for the chr rom
            MemoryStream _ChrRomStream = CreateMemoryStream(RomBankType.VideoRom, _CartStream, _FileInfo);
            // Close the cart stream.
            cart.CloseCart();


            // Get the bank count.
            int _BankCount = _FileInfo.PrgRomBankCount;
            // Load the banks.  Mirror bank one if only one bank.
            if (_BankCount == 1)
            {
                // The cart only has one prg bank.
                _ProgramRomOne = new MapperMemoryBank(this, _ProgramStream, RomBankType.ProgramRomOne|RomBankType.MirrorFlag);
                _ProgramRomTwo = _ProgramRomOne;
                //_ProgramRomTwo = new MapperMemoryBank(this, _ProgramStream, RomBankType.ProgramRomTwo, NesMemoryConstants.PrgRomBankByteSize * -1);
            }
            else if (_BankCount > 1)
            {
                _ProgramRomOne = new MapperMemoryBank(this, _ProgramStream, RomBankType.ProgramRomOne);
                _ProgramRomTwo = new MapperMemoryBank(this, _ProgramStream, RomBankType.ProgramRomTwo, NesMemoryConstants.PrgRomBankByteSize);
            }
            else
            {
                throw new InvalidCartridgeHeaderException(
                    "PrgRomBankCount",
                    _BankCount,
                    "The bank count must be greater than zero !"
                    );
            }

            // Get the char rom bank count.
            _BankCount = _FileInfo.ChrRomBankCount;
            if (_BankCount > 0)
            {
                _VideoRomOne = new PatternTable(this, _ChrRomStream, 0, false);
                _VideoRomTwo = new PatternTable(this, _ChrRomStream, VideoMemoryConstants.PatternTableByteSize, true);
            }
            else
            {
                _VideoRomOne = new PatternTable(this, _ChrRomStream, 0, false);
                _VideoRomTwo = new PatternTable(this, _ChrRomStream, VideoMemoryConstants.PatternTableByteSize, true);
            }
        }

        /// <summary>
        /// Swaps the bank.
        /// </summary>
        /// <param name="romBank">The rom bank.</param>
        /// <param name="index">The index.</param>
        protected void SwapBank(RomBankType romBank, int index)
        {
            // Get the videoRom, programRom, and bank two flags.
            romBank &= RomBankType.VideoRom | RomBankType.ProgramRom | RomBankType.BankTwoFlag;
            //Set the bank offset.
            switch (romBank)
            {
                case RomBankType.ProgramRomOne:
                    _ProgramRomOne.InternalOffset = _ProgramRomOffsets[index];
                    break;
                case RomBankType.ProgramRomTwo:
                    _ProgramRomTwo.InternalOffset = _ProgramRomOffsets[index];
                    break;
                case RomBankType.VideoRomOne:
                    _VideoRomOne.InternalOffset = _VideoRomOffsets[index];
                    #if DEBUG
                    WritePatternImages();
                    #endif
                    break;
                case RomBankType.VideoRomTwo:
                    _VideoRomTwo.InternalOffset = _VideoRomOffsets[index];
#if DEBUG
                    WritePatternImages();
#endif

                    break;
            }
            throw new MapperSwapException();
        }

        /// <summary>
        /// Called when [bank write].
        /// </summary>
        /// <param name="romBank">The rom bank.</param>
        /// <param name="address">The address.</param>
        /// <returns>True if write handled.</returns>
        internal abstract bool OnBankWrite(RomBankType romBankType, ushort address, byte romByte);

        /// <summary>
        /// Reads the stream.
        /// </summary>
        /// <param name="romBank">The rom bank.</param>
        /// <param name="stream">The stream.</param>
        /// <param name="cartFileInfo">The cart file info.</param>
        /// <returns></returns>
        private MemoryStream CreateMemoryStream(RomBankType romBank, Stream stream,
            CartridgeFileInfo cartFileInfo)
        {
            // The offset to read and the number of bytes.
            int _Offset = 0, _Count;
            // The new cart offsets.
            int[] _Offsets;
            //The stream.
            MemoryStream _Stream;
            ///The buffer.
            byte[] _Buffer;
            // Get the program and video rom bits.
            romBank &= RomBankType.VideoRom | RomBankType.ProgramRom;
            // If program rom.
            if (romBank == RomBankType.ProgramRom)
            {
                _Buffer = new byte[NesMemoryConstants.PrgRomBankByteSize];
                // Create the stream.
                _Stream = new MemoryStream(cartFileInfo.TotalProgramRomByteSize);
                // Get the program rom offsets.
                _Offsets = cartFileInfo.PrgRomBankOffsets;
                // Set the program offsets.
                _ProgramRomOffsets = new int[_Offsets.Length];
                // Loop through all the banks.
                for (int _TempIndex = 0; _TempIndex < _Offsets.Length; _TempIndex++)
                {
                    stream.Seek(_Offsets[_TempIndex], SeekOrigin.Begin);
                    stream.Read(_Buffer, 0, NesMemoryConstants.PrgRomBankByteSize);
                    _Stream.Write(_Buffer, 0, NesMemoryConstants.PrgRomBankByteSize);
                    _ProgramRomOffsets[_TempIndex] = _Offset;
                    _Offset += NesMemoryConstants.PrgRomBankByteSize;
                }
            }
            else if (romBank == RomBankType.VideoRom)
            {
                // Create a blank bank for roms that dont have
                if (cartFileInfo.ChrRomBankCount < 1)
                {
                    _VideoRomOffsets = new int[] { 0 };
                    _Stream = new MemoryStream(VideoMemoryConstants.TotalPatternTableByteSize);
                }
                else
                {
                    int _ChrOffsetIndex = 0;
                    _Buffer = new byte[VideoMemoryConstants.TotalPatternTableByteSize];
                    // Create the stream.
                    _Stream = new MemoryStream(cartFileInfo.TotalVideoRomByteSize);
                    // Get the program rom offsets.
                    _Offsets = cartFileInfo.ChrRomBankOffsets;
                    // Set the program offsets.
                    _VideoRomOffsets = new int[_Offsets.Length * 2];
                    // Loop through all the banks.
                    for (int _TempIndex = 0; _TempIndex < _Offsets.Length; _TempIndex++)
                    {
                        // Go to the offset position.
                        stream.Seek(_Offsets[_TempIndex], SeekOrigin.Begin);
                        // Write the pattern tables
                        stream.Read(_Buffer, 0, VideoMemoryConstants.TotalPatternTableByteSize);
                        _Stream.Write(_Buffer, 0, VideoMemoryConstants.TotalPatternTableByteSize);
                        // Write the offsets to the array and increment the offset amount.
                        _VideoRomOffsets[_ChrOffsetIndex] = _Offset;
                        _Offset += VideoMemoryConstants.PatternTableByteSize;
                        // Do again.
                        _VideoRomOffsets[_ChrOffsetIndex + 1] = _Offset;
                        _Offset += VideoMemoryConstants.PatternTableByteSize;
                        // Increment the offset index.
                        _ChrOffsetIndex += 2;
                    }
                }
            }
            else
            {
                throw new MapperInitializationException();
            }
            return _Stream;
        }
        #endregion Methods



    }


}
